Pre-amble: various functions useful for generating slides¶

For convenience, put slides.html generating code at the top:¶

Imports:

show object:

Generate QR-Code's for Demo and Github:

ImageDisplayer objects:

New in PyTeal: Boxes and ABI-Compatible Smart Contracts¶

image.png

Learn how to utilize unlimited global storage and ABI-compatibility in PyTeal, Algorand’s library for writing smart contracts in Python.¶

Agenda
¶

I. PyTeal¶

II. Application Boxes in PyTeal¶

III. PyTeal's ABI-Router: Building an ARC-4 Application¶

IV. Demo¶

V. Questions¶

References
¶

Decipher 2021: Writing Smart Contracts with Python¶

https://www.github.com/jasonpaulos/decipher-22-pyteal-talk¶

In [6]:
github_qr_code()

image.png

.

PyTeal
¶

image.png

.

image.png

.

image.png

image.png

.

Polling dApp
¶

image.png

Favorite Tall Building?¶

1. Empire State - New York¶

2. Burj Khalifa - Dubai¶

3. Abraj Al-Bait - Mecca¶

4. Taipei 101 - Taipei¶

5. Shanghai Tower - Shanghai¶

6. Merdeka 118 - Kuala Lumpur¶

7. Other¶

image.png

.

Polling dApp
¶

Poll administrator can:¶

create and delete the poll application¶

open and close a poll for voting¶

Anyone can:¶

submit a choice¶

get the poll status¶

image.png

.

Example¶

submit choice Burj Khalifa - Dubai¶

After 100 submissions¶

status =¶

  • 11 chose Empire State - New York
  • 35 chose Burj Khalifa - Dubai
  • 22 chose Abraj Al-Bait - Mecca
  • ...

image.png

.

Voting accounts must contribute exactly +1 to tally¶

image.png

image.png

.

Voting accounts must contribute exactly +1 to tally¶

image.png

image.png

.

Voting accounts must contribute exactly +1 to tally¶

image.png

image.png

.

NOTES:

  • Can't keep all this info in global storage
    • Stats - 8KB total possible. Assuming no compression and 32B per address, that's at most 256 accounts we can keep track of
  • Can keep info in local - but then there are some downsides:
    • voters need to opt into poll (MBR issues)
    • need to have policies and code around closing/clearing out
  • TRICKY Sybil attack. Why can't some create tons of accounts and vote many times. AND it's even easier to pull that off with the reduced MBR requirements thanks to boxes...
    • Answer: yes, this isn't meant to be a solution to the general problem of governance voting, etc.
    • This is only meant for low-stake polls

Voting accounts must contribute exactly +1 to tally¶

global storage solution?¶

No. Unlimited voting accounts but 8 KB max¶

local storage solution?¶

Not really. Want lightweight dApp with no opt-in¶

image.png

.

limits reference¶

https://github.com/algorand/go-algorand/blob/995ae47e80c50e7632034cac8a70b7d6434d03e3/config/consensus.go#L969-L970

64 keys X (64 + 64 bytes) == 8,196 bytes

min MBR = 0.15 Algos with 1 local¶

complex code/policies for opt in, close out, clear state¶

Anything else we could do?¶

Introduce voting token¶

Yes, BUT requires asset opt-in and aiming for lightweight app¶

Notes:

  • expensive 0.1 Algos / acct vs. 0.0157 Algos / acct w/ boxes
  • complicated / error prone
    • need to keep track of asset id off chain and supply it as a foreign ref, then parse out URL, verify that first 32 bytes are the account, etc...
    • need to group with Asset Config Txn which specifies how the URL changes

SOLUTION via Application Boxes
¶

image.png

Notes:

  • In this solution, only need
    • 32 bytes for the Key (max is 64 bytes)
    • 1 byte for the value (max is 32 KB)
  • Costs in terms of MBR formula of 2500 + 400*(len(key) + len(value))
    • 15.7 mili-Algo's to store submission info per account
    • Each txn allows up to 8 box references in it with the size of boxes touched totalling at most 1KB * # of refs
    • In our case, this is not an issue. A single box reference -being the submission account- will suffice

image.png

.

Application Boxes in PyTeal
¶

image.png

.

Notes

  • create: create a box of specified size, with a NoOp if already exists. Returns an indicator for whether box actually got created
  • length: when the box exists, gives its value's size. When it doesn't its hasValue() will be false
  • delete: don't forget to do this as apps don't clean up their own boxes!!! Returns indicator for whether box actually got deleted
  • replace: ... nothing to add ...
  • extract: ... nothing to add ...
  • put and get: the workhorses we'll see in action shortly
  • put: for setting the entire contents of a box, creating it when it doesn't exist. Returns indicator for whether a box was created during execution
  • get: get the entire contents of a box. hasValue() will be false if the box doesn't actually exist

PyTeal App.box_*() API
¶

App.box_create(name, size) - any size ($\leq$ 32KB)¶

App.box_length(name) - opcode box_len¶

App.box_delete(name) - opcode box_del¶

App.box_replace(name, idx, L) - set part of box¶

App.box_extract(name, idx, L) - get part of box¶

App.box_put(name, value) - set value (may create box)¶

App.box_get(name) - get everything (fail if size > 4KB)¶

image.png

.

App.box_get() and App.box_put()¶

Notes

Inspired by

@router.method
def submit(choice: abi.Uint8) -> Expr:
  • sender_box is a MaybeValue:
    • sender_box.has_value() <--> box exists
    • sender_box.value() --> (when box exists) full contents
In [7]:
from pyteal import Itob, Int, Seq, App, Txn, If
# App.box:    account --> choice
# App.global: choice  --> count

# for real app, choice is passed in as parameter.
choice = Itob(Int(1))  # 1: index of "Burj Khalifa - Dubai"

submit_expr = Seq( # switch vote old_choice ---> choice
  old_choice := App.box_get(Txn.sender()),
  If(old_choice.hasValue()).Then(
    App.globalPut( # App.global[old_choice] -= 1
      old_choice.value(),
      App.globalGet(old_choice.value()) - Int(1),
    ),
  ),
  App.box_put(Txn.sender(), choice),  # App.box[account] <--- choice
  App.globalPut(   # App.global[choice] += 1
    choice, 
    App.globalGet(choice) + Int(1),
  ),
)

image.png

.

PyTeal's ABI-Router: Building an ARC-4 Application¶

image.png

.

methods - allow interacting with Poll App¶

* create - called only during app creation and only by administrator¶

* open - called only by administrator¶

* close - called only by administrator¶

* submit¶

* status¶

(didn't include delete for reasons to be explained shortly)¶

image.png

.

Refresher - OnComplete Actions for App Transactions¶

image.png

image.png

.

Value Name Description
0 NoOp Execute ApprovalProgram only
1 OptIn Allocate local state and execute ApprovalProgram
2 CloseOut Execute ApprovalProgram and clear local state
3 ClearState Execute ClearStateProgram and clear locals (even if rejects)
4 UpdateApplication Execute ApprovalProgram and update programs
5 DeleteApplication Execute ApprovalProgram and delete the app

ABI method: code for app transaction with first argument the selector¶

ABI bare app call: app transaction with no arguments. Its action is the code to be executed¶

For Polling dApp:¶

delete is a bare app call for OnComplete=DeleteApplication¶

image.png

.

PyTeal Router and its M.O.E. Questions¶

In [8]:
moe()

image.png

.

The Router delegates to appropriate method or bare app call action based on:¶

[M] In a bare app call? If not, which method selected?¶

[O] Which OnComplete is requested?¶

[E] This app already exists? (Conversely, being created?)¶

image.png

.

Router Compilation into an ABI Smart Contract¶

image.png

image.png

.

Router Initialization¶

In [9]:
warning()
In [10]:
# WARNING: STUBS ARE FOR ROUTER-ILLUSTRATION PURPOSES ONLY!!!
del_action = OnCompleteAction.call_only(Seq()) 

router = Router(
    name="OpenPollingApp",
    descr="This is a polling application.",
    bare_calls=BareCallActions(delete_application=del_action),
)

image.png

.

Add methods with @router.method decorator¶

In [11]:
@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.""" # <-- Docstring
    return Seq()

ABI provides standard for encoding variety of data types¶

abi.Uint8 - An 8-bit unsigned integer encoded as big-endian bit sequence¶

There are more: Uint16/32/64, Bool, Byte, String, StaticArray, DynamicArray, Tuple, ...¶

Link: PyTeal ABI Types
¶

image.png

.

Compile¶

In [12]:
opts = OptimizeOptions(scratch_slots=True)
(approval, clear, json_contract) = \
    router.compile_program(version=8, optimize=opts)
# ------------------------------------^^^^^^^^^^^^^

image.png

.

JSON contract¶

In [13]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice"
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}

image.png

.

Routing Nitty Gritty¶

image.png

.

In [15]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l4
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l3
err
main_l3:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_0
int 1
return
main_l4:
txn OnCompletion
int DeleteApplication
==
bnz main_l6
err
main_l6:
txn ApplicationID
int 0
!=
assert
int 1
return

// submit
submit_0:
store 0
retsub

Routing method submit(choice: abi.Uint8)¶

In [17]:
show() # approval
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l3
. . .
main_l3:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_0
int 1
return
. . .
// submit
submit_0:
store 0
retsub

image.png

.

Routing bare call for delete¶

In [19]:
show() # approval
txn NumAppArgs
int 0
==
bnz main_l4
. . .
main_l4:
txn OnCompletion
int DeleteApplication
==
bnz main_l6
. . .
main_l6:
txn ApplicationID
int 0
!=
assert
int 1
return

image.png

.

DEMO
¶